Skip to content

refactor(clone): type-driven clone defaults with honor-or-throw decorators#3040

Merged
GuoLei1990 merged 130 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/clone-opt-out-assignment
Jul 24, 2026
Merged

refactor(clone): type-driven clone defaults with honor-or-throw decorators#3040
GuoLei1990 merged 130 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/clone-opt-out-assignment

Conversation

@cptbtptpbcptdtptp

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Rework the clone system around a type-driven priority chain, so most objects clone correctly by their type with little to no per-field annotation.

How a field value is cloned — priority high → low

  1. Field decorator@deepClone / @assignmentClone / @ignoreClone, consulted wherever the clone walks fields (the component top level and inside any object that is itself deep cloned). A decorator is an explicit intent: @deepClone pointed directly at a value that cannot be deep cloned — an Entity/Component reference, an asset, engine runtime state, a function, or an unregistered opaque platform object — throws instead of silently falling back.
  2. Type default — resolved from the value itself:
    • Primitives → copy. Functions → the clone keeps its own constructor-bound function, else shares the source's.
    • Entity / Componentremap through the clone identity map (in-subtree → the corresponding clone; outside → the original reference).
    • ReferResource assets (Texture / Mesh / Material / Sprite / Font / …) → share.
    • UpdateFlagManager / UpdateFlagkeep the clone's own (a flag and its manager hold each other; a field copy resolves neither side) — @deepClone on them throws.
    • DisorderedArray / SafeLoopArray → keep the clone's own by default; as plain containers, @deepClone may deep-copy them.
    • Containers (Array / Map / Set / TypedArray / DataView / plain & null-prototype objects) → deep — members re-enter the gate, Map keys included.
    • Internally registered math value types (Vector*, Matrix*, Color, Ray, …) → copy via their type-owned copyFrom implementation. Merely defining a method named copyFrom does not opt a custom type in.
    • DataObject subclasses → deep via field walk (27 engine classes: particle modules & curves, shader render states, joint config, PostProcess, ShaderData, Signal, ColliderShape, ui Transition, Skin, …).
    • Anything else → share.

@deepClone deep-clones the whole subtree

The deep intent propagates through containers and field-cloneable ordinary members. Field cloning deliberately covers only own enumerable string-keyed properties; private fields, non-enumerable properties, Symbol keys, and prototype/accessor state are outside the contract. Assets stay shared, entity references remap, runtime state keeps the clone's own, unregistered opaque members keep their assignment default, and nested field decorators still win. Only the value the decorator directly sits on is subject to honor-or-throw.

How a type opts into deep-by-default

A custom structural data type opts in by extending DataObject (public marker base class). Engine math value types are registered internally with a Symbol-backed Copy default because math cannot depend on core; a user-defined copyFrom method alone is not an opt-in. Copy/Deep types require argument-less construction when no compatible preset exists: the gate bare-constructs the target and then populates it, and reports a named error if that fails.

Ref-count model — slot-ownership contract

  • Every component top-level field sharing a ReferResource owns one reference: the gate acquires it while cloning the slot (+1, and −1 when displacing an owned constructor preset — _transferSlotOwnership). Releasing it on destroy is the owning class's responsibility.
  • Below the top level the gate never counts. Nested hosts pair acquisition and release in class-local code: Transition._onClone +1 ↔ Transition.destroy() −1 (all four state slots in the base class); ShaderData.cloneTo cascades by the host's refCount, texture-array entries included.
  • Slots rebuilt through setters stay @ignoreClone so the setter is the single +1 source (MeshRenderer.mesh, Renderer materials, MeshColliderShape.mesh, particle MeshShape.mesh).
  • Assets referenced by custom script fields are kept alive by their clones; free such an asset via its own destroy().

Changes

  • CloneDecorators is the single owner of the internal Symbol metadata and registrations; execution lives in CloneUtil. Public clone API = the three field decorators + DataObject; default modes and their Symbol keys stay internal.
  • −200 / +9 field decorators (the 9 additions are genuine runtime-state opt-outs, e.g. Signal._listeners, MeshColliderShape._mesh). ⚠️ @shallowClone removed (breaking) — migrate with @deepClone / @assignmentClone. CloneUtils and the srcRoot/targetRoot path-walk remap removed — Entity/Component remap unified through the clone identity map; _onClone hooks receive (target, cloneMap).
  • ⚠️ @assignmentClone / @ignoreClone on Entity/Component refs are honored literally (share / keep own) instead of being silently overridden by auto-remap; @deepClone on an unhonorable value throws instead of warning and falling back.
  • The field walk iterates Object.keys (own keys) instead of for...in: the loose ES5 bundle makes prototype methods enumerable, so for...in copied them onto every clone as own properties — clones now match a fresh instance's shape (MeshRenderer 61 → 31 own keys), and a 20-entity tree clones 4× faster (0.39 → 0.09 ms).
  • Every producing branch registers its clone in the identity map before descending: aliasing (one value referenced from N slots), self-referencing cycles, and _onClone hooks running exactly once per source object are all pinned by tests.
  • ShaderData joins the deep family with a _onClone hook: a renderer's shaderData is now actually cloned — previously all of its fields were @ignoreClone with no hook, so renderer clones silently lost floats and macros. Texture-array entries cascade through _addReferCount like single textures (review 4629713846). CustomDataModule drops its hand-written _onClone — the container defaults reproduce it, ShaderProperty identity preserved.
  • MeshColliderShape clones cook a native shape through the mesh setter and attach exactly once (_isShapeAttached bookkeeping hoisted to ColliderShape for Collider's generic attach/detach path).
  • Docs (docs/en|zh/core/clone.mdx, how-to-contribute) rewritten to the final semantics: default-rules table (functions and Map keys included), the public DataObject opt-in and internally registered math Copy types, the enumerable string-field contract, subtree propagation, ref-count contract, and @shallowClone migration callout.

Pre-existing imbalances fixed alongside (exposed by the ref-count rework)

TextRenderer._font / ui Text._font (destroy released a count the clone never acquired) and MeshColliderShape._mesh (same, plus the cloned shape silently lacked a native shape) were unbalanced on dev/2.0 before this PR; the slot-ownership contract requires them fixed here to stay self-consistent. Destroying a ParticleRenderer also never released a MeshShape's mesh — fixed via a BaseShape._destroy hook on the generator destroy chain.

Test plan

  • Full local suite after merging latest dev/2.0: 1653 tests / 122 files pass (core / ui / loader / math / PhysX physics); pnpm run b:all also passes.
  • Contract suites: bare-construction inventory, decorator honor-or-throw (entity / asset / runtime state / function), subtree propagation (negative-verified against the pre-change gate), aliasing & cycle topology, shared-object _onClone exactly-once, binary preset reuse, Map key cloning, template-source counting
  • refCount lifecycle suites: slot-ownership churn per counted slot, ShaderData/Material cascade incl. texture arrays, particle MeshShape clone round-trip, ui Transition / UIInteractive full lifecycle
  • Two multi-agent adversarial review rounds (6 dimensions, then clone-core / ref-count / 50-file decorator sweep / docs-tests) — every P1/P2 finding fixed or explicitly deferred with rationale
  • CI on latest head

Merge 决策记录 — 7ba6d16f3(dev/2.0 同步合并)

本次同步合并在文本冲突解决之外,有意修改了三处上游代码行(可通过 git show 7ba6d16f3 的 combined diff 查看)。三处删除在类型驱动闸门下均行为等价:

  1. Animator.ts — 删除 fix(animation): harden animator state instance data #3024 新增的 fireEvents 上的 @assignmentClone 该装饰器进入本分支时处于"使用但未导入"状态(本分支已删除该文件的对应 import),不删会在 shader-compiler 预编译时抛 ReferenceError;布尔原始值在闸门下天然按值拷贝,装饰器冗余。
  2. VelocityOverLifetimeModule.ts — 删除 feat(particle): add orbital radial velocity over lifetime #3049 新增的 5 个 @deepClone(orbital 曲线 ×4 + _offset)。 ParticleCompositeCurveVector3 经类型默认即深拷,与同文件已去装饰器的 _velocityX/Y/Z 口径一致。由测试 "orbital velocity fields deep-clone through the type default" 钉住。
  3. ParticleCompositeCurve.ts — 删除上游为 _updateManager 补的 @ignoreClone UpdateFlagManager 已按类型默认 Ignore,字段装饰器冗余。

🤖 Generated with Claude Code

…ainer elements

- Add `@defaultCloneMode(mode)` class decorator for declaring how instances
  of a type should be cloned when encountered as field values
- Change container element cloning: array elements now use `undefined` as
  cloneMode so each element's type-level `_defaultCloneMode` is consulted.
  Unknown types (no _defaultCloneMode) default to Assignment (shared reference)
  instead of Deep, preventing crash on DOM/native objects.
- Add `_defaultCloneMode` to ICustomClone interface
- Mark RenderState system classes with @defaultCloneMode(Deep):
  DepthState, StencilState, RasterState, RenderTargetBlendState, BlendState, RenderState

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The clone system now uses class-level default clone modes, clone-map-based identity tracking, and updated resource/reference-count handling across core engine types. Particle, physics, UI, and post-process components were adjusted to the new clone behavior, and the clone-related tests and docs were expanded accordingly.

Changes

Clone pipeline and core remapping

Layer / File(s) Summary
Clone infrastructure
packages/core/src/clone/CloneManager.ts, packages/core/src/clone/ComponentCloner.ts, packages/core/src/clone/enums/CloneMode.ts, packages/core/src/index.ts, packages/core/src/clone/CloneUtils.ts
CloneMode adds Remap, removes Shallow, and updates its docs. CloneManager adds defaultCloneMode, per-field mode registration, getFieldModes, _cloneValue, _deepClone, and counted-resource helpers, while CloneUtils is removed. ComponentCloner switches to cloneMap-based cloning and the revised ICustomClone contract.
Entity, component, and signal remapping
packages/core/src/Component.ts, packages/core/src/Entity.ts, packages/core/src/Signal.ts, packages/core/src/particle/modules/CustomDataModule.ts
Component and Entity move to @defaultCloneMode(CloneMode.Remap) and use a shared clone map through subtree cloning. Signal now remaps listeners and structured-binding arguments from the same identity map, and CustomDataModule threads that map through deep-cloned curves and gradients.

Clone-mode updates across engine, particle, physics, and UI classes

Layer / File(s) Summary
Core clone-mode updates
packages/core/src/shader/state/*, packages/core/src/Camera.ts, packages/core/src/Renderer.ts, packages/core/src/Transform.ts, packages/core/src/UpdateFlag*.ts, packages/core/src/VirtualCamera.ts, packages/core/src/animation/Animator.ts, packages/core/src/asset/ReferResource.ts, packages/core/src/audio/AudioSource.ts, packages/core/src/lighting/Light.ts, packages/core/src/mesh/Skin.ts, packages/core/src/mesh/SkinnedMeshRenderer.ts, packages/core/src/postProcess/*, packages/core/src/shader/ShaderData.ts, packages/core/src/trail/TrailRenderer.ts, packages/core/src/utils/*, packages/math/src/Ray.ts
Core engine types move to class-level default clone modes or remove explicit field decorators. Several classes add or remove clone-time cleanup and resource ownership behavior, and Ray now implements clone() and copyFrom().
Particle, physics, and UI clone behavior
packages/core/src/particle/*, packages/core/src/particle/modules/*, packages/core/src/particle/modules/shape/*, packages/core/src/physics/*, packages/ui/src/component/*
Particle modules and shape classes migrate to class-level defaults, runtime-only fields are ignored, and mesh/state teardown hooks are added. Physics and UI components also update clone decorators, cleanup, and reference-count handling to match the new pipeline.

Clone and ref-count test coverage

Layer / File(s) Summary
Clone, ref-count, and copy coverage
tests/src/core/*, tests/src/math/Ray.test.ts, tests/src/ui/*
New and expanded tests cover clone-map remapping, slot ownership and ref-count balancing, binary data cloning, function handling, math value-type copy/clone, and UI/physics transition behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: GuoLei1990

Poem

🐇 I hopped through clone maps, tidy and bright,
With deep-clone defaults and remaps just right.
Ref counts now dance and the tests sing along,
The bunny says: “cloning feels sturdy and strong!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main refactor: type-driven clone defaults plus decorator-driven clone behavior changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/clone/CloneManager.ts`:
- Around line 67-69: The defaultCloneMode decorator accepts any CloneMode value,
but the runtime logic at lines 141-149 only properly handles Deep and Shallow
modes, while Ignore is checked separately earlier at line 138. To fix this,
modify the defaultCloneMode function signature to restrict the mode parameter to
only accept CloneMode.Deep or CloneMode.Shallow (using a union type or
overload), preventing callers from incorrectly decorating with CloneMode.Ignore
which would not be honored at runtime.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 585fad6c-db8f-4c53-9512-ff3e147e9dde

📥 Commits

Reviewing files that changed from the base of the PR and between 5d74c1d and 79e9105.

📒 Files selected for processing (8)
  • packages/core/src/clone/CloneManager.ts
  • packages/core/src/clone/ComponentCloner.ts
  • packages/core/src/shader/state/BlendState.ts
  • packages/core/src/shader/state/DepthState.ts
  • packages/core/src/shader/state/RasterState.ts
  • packages/core/src/shader/state/RenderState.ts
  • packages/core/src/shader/state/RenderTargetBlendState.ts
  • packages/core/src/shader/state/StencilState.ts

Comment thread packages/core/src/clone/CloneManager.ts Outdated
cptbtptpbcptdtptp and others added 7 commits June 17, 2026 19:31
…+ type-driven deep clone

Core changes:
- Clone is now opt-out: all enumerable fields are cloned unless @ignoreClone
- Default clone mode for unknown types is Assignment (shared reference) — safe
  for DOM elements, native handles, and any unrecognized constructor
- Types that need independent copies declare @defaultCloneMode(CloneMode.Deep)
- Identity-map based deep clone with cycle/shared-subgraph dedup
- 3-stage lifecycle: Construct → Populate (copyFrom or for...in) → Finalize (_cloneTo)
- Container elements (Array/Map/Set) go through the type-driven gate individually
- @deepClone/@assignmentClone/@shallowClone kept as no-op for backward compat
- @ignoreClone remains functional

RenderState classes marked @defaultCloneMode(Deep):
  DepthState, StencilState, RasterState, RenderTargetBlendState, BlendState, RenderState

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eep)

Particle system:
  ParticleGeneratorModule (+ all subclasses via inheritance),
  MainModule, BaseShape (+ all shape subclasses), ParticleGenerator,
  ParticleCompositeCurve, ParticleCurve, CurveKey,
  ParticleCompositeGradient, ParticleGradient, GradientColorKey,
  GradientAlphaKey, Burst

Post-process:
  PostProcessEffect (+ BloomEffect, TonemappingEffect via inheritance),
  PostProcessEffectParameter (+ Float/Bool/Color/Vector/Enum/Texture)

Physics:
  JointLimits, JointMotor

Other:
  VirtualCamera, Skin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mark Entity/Component with @defaultCloneMode(Remap) and split Entity.clone() into a
register-then-copy two-pass: pre-register every source Entity/Component to its clone in
the identity map across the whole subtree, so references nested in arrays/maps/objects
are remapped through the clone gate, not just top-level fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Re-register @deepClone/@assignmentClone/@ignoreClone as field-level modes (highest priority).
- Containers (Array/Map/Set/TypedArray/plain object) default to deep clone.
- Type defaults: ReferResource -> Assignment, math types -> Deep, UpdateFlagManager -> Ignore.
- Thread srcRoot/targetRoot through the gate so Signal._cloneTo can remap listeners.
- Add CloneMode.Ignore; drop erroneous @deepClone on Joint limits/motor _updateFlagManager.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…efaults

- Remove @deepClone/@assignmentClone/@shallowClone field decorators (171) across core & ui.
- Fields resolve via container default deep + type-level @defaultCloneMode + Assignment fallback.
- Mark ShaderData & Signal Deep; ShaderData adds _cloneTo delegating to cloneTo.
- Clean up now-unused clone decorator imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t-assignment

# Conflicts:
#	packages/core/src/particle/ParticleGenerator.ts
…l run

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.53811% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.40%. Comparing base (8fe0657) to head (24017e7).

Files with missing lines Patch % Lines
packages/core/src/clone/CloneUtil.ts 99.20% 3 Missing ⚠️
packages/core/src/animation/AnimatorController.ts 50.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3040      +/-   ##
===========================================
+ Coverage    85.22%   85.40%   +0.18%     
===========================================
  Files          810      811       +1     
  Lines        94673    94654      -19     
  Branches     11342    11507     +165     
===========================================
+ Hits         80684    80839     +155     
+ Misses       13899    13725     -174     
  Partials        90       90              
Flag Coverage Δ
unittests 85.40% <99.53%> (+0.18%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

cptbtptpbcptdtptp and others added 6 commits June 23, 2026 15:40
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…low)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…remap via identity map

- Assignment is now a plain reference share: the gate never touches refCount.
  Ref-count ownership belongs to each class's own logic (_cloneTo hooks /
  setters), balanced by that class's destroy path. Fixes double-counting on
  Camera/Animator/AudioSource clones, Shader leak via _replacementShader, and
  permanent leaks for user-script-held resources.
- Field decorators are highest priority at all depths: the ComponentCloner
  top-level remap special case is removed; Entity/Component remap unified
  through the pre-populated cloneMap (O(1)); CloneUtils path-walk deleted.
  @deepClone on an Entity/Component ref warns and falls back to remap.
- Entity.clone() builds the tree and registers identity pairs in one walk;
  srcRoot/targetRoot threading removed — _cloneTo hooks receive cloneMap.
- Container classification centralized (single _isContainer); DataView clones
  as bytes (was: crash via generic object branch); functions in containers are
  shared instead of dropped to undefined, while constructor-rebound top-level
  handlers keep the clone's own binding.
- Register ColliderShape / ui Transition as @defaultCloneMode(Deep) so cloned
  colliders/interactives own independent instances; restore @ignoreClone +
  setter pattern for TextRenderer/Text fonts and MeshColliderShape mesh;
  SpriteTransition acquires state-sprite refs on clone.
- shallowClone decorator warns on use; dead copyProperty/copyProperties
  removed; CloneMode exported; orphan imports cleaned.
- Tests: decorator-priority semantics updated; new regressions for function
  fields, DataView, refCount balance (RT/controller/font/sprite-transition),
  collider-shape and transition instance independence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve tests/src/core/physics/ColliderShape.test.ts: physics-lite describe
removed upstream; move the shape-independence regression into the PhysX
describe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cptbtptpbcptdtptp

Copy link
Copy Markdown
Collaborator Author

Pushed a follow-up that reworks two load-bearing decisions of this PR (design discussion happened offline):

1. The clone gate no longer touches refCount. Assignment is a plain reference share again. The gate's unconditional +1 was structurally unbalanceable: it double-counted with the manual +1s in Camera/Animator/AudioSource._cloneTo, leaked on fields no destroy path releases (user Script resources, Camera._replacementShader — Shader isn't even a ReferResource), and couldn't reach non-component hosts (SpriteTransition state sprites, Signal args). Ownership now lives entirely in each class's own logic (_cloneTo hooks / setters), balanced by that class's destroy path — the proven pre-existing pattern (MeshRenderer, Renderer, ShaderData.cloneTo). Fixed the classes that had drifted from it: TextRenderer._font / ui Text._font / MeshColliderShape runtime state are @ignoreClone + setter-in-_cloneTo; SpriteTransition._cloneTo acquires its state-sprite refs.

2. Remap is unified through the identity map and field decorators win at every depth. The ComponentCloner "_remap first" special case and the CloneUtils path-walk are gone — Entity.clone() registers every source→clone pair while building the tree (2 walks instead of 3), and the gate's Remap branch resolves in O(1) with identical out-of-subtree semantics. srcRoot/targetRoot threading is removed; _cloneTo hooks receive the cloneMap (only Signal uses it). @deepClone on an Entity/Component ref warns and falls back to remap instead of fabricating an engine-less instance. ⚠️ Semantic change: @assignmentClone/@ignoreClone on entity refs are now honored literally instead of being overridden by auto-remap (tests updated).

Also in this push: DataView clones as bytes (crashed before via the generic object branch); functions inside containers are shared instead of silently becoming undefined, while constructor-rebound top-level handlers keep the clone's own binding; container classification is a single _isContainer predicate; ColliderShape / ui Transition are registered @defaultCloneMode(Deep) so cloned colliders/buttons own independent instances (previously the clone shared and re-parented the source's shapes/transitions); dead copyProperty/copyProperties removed; shallowClone warns on use; CloneMode is exported.

Known pre-existing (not addressed): ColliderShape's constructor presets a native PhysicsMaterial that gets orphaned whenever the field is later replaced (clone or user shape.material = x) — same behavior on dev/2.0, worth its own issue.

Tests: core 907+ / physics / ui+loader all green locally; new regressions cover function fields, DataView, refCount balance (RT / controller / font / sprite-transition / script-held textures), and instance independence for collider shapes & transitions. Branch merged with latest dev/2.0 (resolved ColliderShape.test.ts against the physics-lite removal).

🤖 Generated with Claude Code

cptbtptpbcptdtptp and others added 10 commits July 3, 2026 11:09
…eleases

Component top-level fields sharing a registered ref-counted resource
(ReferResource family) acquire one reference at the clone gate (+1, and
-1 when replacing an owned preset); the owning component's destroy path
releases it (implementation contract). Scripts have no per-field destroy
logic, so gate acquisitions are recorded in a ledger and released in
Script._onDestroy. Below the top level the gate never counts: container
elements and plain-object fields are plain shares, and nested classes
pair the acquisition themselves (SpriteTransition._cloneTo +1 paired
with Transition.destroy -1, hoisted to the base class so future
ReferResource-valued transitions inherit the release).

Manual +1 compensations in Camera/Animator/AudioSource._cloneTo are
removed (the gate acquisition replaces them); TextRenderer/Text._font
return to gate accounting. The counted-resource test is explicit
registration (@defaultCloneMode(Assignment)), excluding duck-typed
counters like Shader.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…leak

Add slot-ownership contract suites asserting the full lifecycle
(baseline → clone +1 → setter churn → destroy release) for every
counted component slot: Camera.renderTarget, Animator.controller,
AudioSource.clip, SpriteRenderer/SpriteMask/ui Image sprite,
MeshRenderer.mesh, MeshColliderShape.mesh, SpriteTransition states,
and the Script ledger (reassignment still releases the original).
Add ShaderData/Material cascade suites: setTexture swap/clear,
doubly-referenced hosts (±refCount propagation), renderer clone
balance, and instance-material lifecycles.

The new suites exposed a pre-existing leak: destroying a
ParticleRenderer never released the MeshShape's mesh (+1 in the mesh
setter, no release path). Add BaseShape._destroy, called from
EmissionModule._destroy, with MeshShape releasing through its setter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shallow is no longer a distinct mode; keeping the decorator silently
behaving as deep clone hides the semantic change. Migrate with
@deepClone (independent copy) or @assignmentClone (share the
reference).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Public API (getFieldModes, deepCloneObject) first, @internal entries
(_registerFieldMode, _cloneValue) after, private helpers last.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Script fields are user territory: the engine cannot pair a release for
an automatic acquisition (users can't know which slots were acquired),
so the gate does no counting for script slots at all — replacing the
clone-acquisition ledger. Users who need gc protection manage counts
themselves, matching the pre-2.0 semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cloning's only ref-count job is acquiring the count for the slot it
copies. Releasing on destroy is the owning class's existing contract:
engine components in their destroy paths, script authors in onDestroy.
Remove the script exemption flag; Script.ts is untouched by this PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_cloneValue is pure cloning again (4 params, Assignment = plain
share). ComponentCloner — the only place slots own references —
detects the share (result === source value, registered resource
only; remapped/deep results never match) and acquires through
CloneManager._acquireSlotOwnership.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Not a priority rule: deep-copying an Entity/Component is an
unexecutable directive, so it recovers to remap with a warning.
Every executable decorator still wins over the type default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep the model summary and the ref-count contract in two short
paragraphs; per-type behavior and deep-clone stages live on the
methods themselves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…shared clone

[vec3, vec3, vec3] must clone into one NEW Vector3 referenced three
times (identity-map dedup), not three copies and not the source
instance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cptbtptpbcptdtptp and others added 2 commits July 19, 2026 23:57
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last hole in honor-or-throw: pointing the decorator at a function silently
shared the source's binding. Code is not a cloneable graph, so the assert now
rejects it like entities, assets, and runtime state; a function reached by a
propagated deep intent inside the subtree still shares, unthrown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cptbtptpbcptdtptp cptbtptpbcptdtptp changed the title refactor(clone): type-driven default clone mode for container elements refactor(clone): type-driven clone defaults with honor-or-throw decorators Jul 19, 2026
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

增量审查 @ c61e0684(第四十一轮)— delta = 18 commit(真行为变更批次@deepClone 语义改为深入子树 + for...inObject.keys 修 clone 走原型方法 + engine-runtime 分支拆分 + _createCloneTarget 抽取 + CloneManager 停止 re-export + 一批注释/文档收敛)— ✅ 两处高风险行为变更经对抗验证 + 反向可证伪测试 + CI 12/12 全绿逐条坐实为安全(其中两处实为 correctness 修复),无 P0/P1,LGTM

总结

自上轮 0494dca5(我第四十轮 --comment LGTM 点)起 tip 线性推进 18 个 commitcompareahead:18 / behind:0merge_base=0494dca56、status ahead无 force-pushgit ls-remote refs/pull/3040/head 校准真 tip=c61e0684e4c9befb688c743be02c6e11b64d6e8fgh pr view headRefOid 一致)。与第 31~40 轮的纯注释 delta 不同,本批含多处真行为变更,逐一深审如下。碰 20 文件,核心是 CloneUtil.ts(+110/-106) + Signal.ts + 365 行新测。CI build×3 + e2e×4 + codecov×3 + lint + labeler = 12/12 全绿

1. for...inObject.keys(perf 702d1a79)—— 逐链核实为 correctness 修复,非数据丢失

_deepCloneObject / ComponentCloner.cloneComponent 的字段遍历从 for (const k in source)(含继承的可枚举属性)改为 Object.keys(source)(仅自有可枚举)。commit body 论断"release bundle 用 swc loose+es5 downlevel,把原型方法 emit 成可枚举的 prototype.name = fn,故 for...in 把原型方法当字段拷成 clone 自有属性(cloned MeshRenderer 61 own keys vs fresh 31,ShaderData 41 vs 6)"。对抗验证坐实(mechanism-level)

  • packages/**/*.ts enumerable: true、零原型数据赋值(唯一的 prototype.*=Polyfill.ts 里 feature-gated 的函数 polyfill)。
  • 所有实例数据 = class field(_foo = value),swc emit 为 ctor 内 this._foo=(自有属性)→ Object.keys 全捕获,零丢失。核了 Renderer/MeshRenderer/Transform/ShaderData/Component/EngineObject/DisorderedArray/SafeLoopArray 全部字段。
  • 继承的可枚举属性 100% 是 loose-mode 原型方法 + constructor(clone 本就不该拷成 own props)。
  • _fieldModesObject.definePropertyenumerable → 默认 false)、class getter(accessor enumerable:false)both 遍历都不走,无变化。
  • Math 值类型走 copyFrom 分支绕过 field-walk,原型可枚举性无关。

结论:Object.keys 恰好丢弃且仅丢弃 loose-mode 原型方法 —— clone 不再携带原型方法作为 own props,是修复而非回归。e2e 克隆真实 prefab 全绿佐证无数据字段丢失。

2. @deepClone 深入子树(feat 64410a6a)—— 无生产回归,Signal shield 正确必要

语义变更:@deepClone 的深拷意图从"止于装饰字段(容器得空壳持共享成员)"改为"传播进成员"(forceDeepClone_deepCloneObject 线程进每个子 _cloneValue),子树内"软"处理(asset 共享 / entity remap / runtime state 保 clone 自有 / function 共享不 throw);honor-or-throw 移入 _cloneValue_assertDeepCloneable,只作用于装饰值本身,不作用于传播的子节点。commit body 框为恢复 pre-#1682 的递归行为(#1682 rewrite 意外改成 stops-at-field)。对抗验证坐实

  • 零生产回归可能@deepClone 在一方 engine 源码 packages/*/src 零使用(仅测试 fixture)。forceDeepClone 生产路径永不为 true,故传播变更不可能回归任何现有一方消费者 —— 这是给三方/编辑器代码的 latent-correctness 变更。
  • Signal._listeners @ignoreClone shield 正确必要:逐链 trace —— 无 shield 时传播的 @deepClone 到 Signal(extends DataObject→field-walk)会走 SafeLoopArray 分支深拷 listener 对象,再 _cloneTo 在其上 rebuild = 双份 state;@ignoreClone(本 PR 新加 Signal.ts:13)跳过 walk 精准封死,配 :1044 回归测试。
  • 无其它未 shield 的 SafeLoopArray/DisorderedArray:穷举所有此类字段 + _cloneTo hook,Signal._listeners 是唯一可被传播深拷意图作为成员触达的 value-type 数组(已 shield);其余全在 Entity/Component/managers(remap 或永不 field-walk)。无遗留双份 state bug。

反向可证伪测试覆盖完整:@deepClone subtree propagation(plain-class 成员深拷 / force 穿透嵌套 plain object / engine-bound 成员保默认)、function inside subtree shared@deepClone overrides default on plain container(DisorderedArray force field-walk)、@deepClone on UpdateFlagManager throwsBagHolderScript(force 仍尊重 Bag 自己的 @ignoreClone)。

3. engine-runtime 分支拆分(fix 36472b39)—— 语义分类修正

base 把 UpdateFlagManager|UpdateFlag|DisorderedArray|SafeLoopArray 混为一个"engine runtime state"分支(force 全 throw)。tip 拆分:UpdateFlagManager|UpdateFlagreturn preset(不可 override,flag/manager 互持、field copy 解不了任一侧,throw 在 _assertDeepCloneable);DisorderedArray|SafeLoopArray→独立分支(plain container,force 时 field-walk 正确深拷)。分类正确 —— 前者是真正的 transient 运行时耦合,后者只是 plain container。测试逐一坐实(undecorated 保 clone 自有 / forced 深拷 / @deepClone on UpdateFlagManager throws)。

4. _createCloneTarget 抽取(refactor 3d06a6d2/729573550)+ fold-branch(b0f8226f)—— 等价、cycle-safe 保持

_createCloneTarget 解析目标实例(复用 preset 或 bare-construct)并在返回前 cloneMap.set(source, dst):150),identity lookup 留在 call site:已克隆对象经 call-site 的 cloneMap.get 早退,不重放 populate + _cloneTo hook(3d06a6d2 body 明说 hook 会 acquire refs + register listeners,重放静默翻倍,配新测 pin)。cycle-safety lookup → set → recurse 逐分支保持。

5. CloneManager 停止 re-export(fix 983fe599)—— 真跨包 .d.ts correctness 修复

CloneManager 类级 @internal + core tsconfig stripInternal:true → emit 的 .d.ts 抹除该类,index.tsexport { CloneManager, ... } 悬空 = TS2305(无 skipLibCheck 的消费者)。只有三个装饰器是 public API,一方零消费者 import 该类。与第 37 轮 DataObject 同型的真修复,commit body 准确。

6. 一批注释/文档收敛 + ShaderData._addTexturesReferCount static→instance(a1eb05c8)—— 等价

  • ShaderData._addTexturesReferCount:body 只碰 property._addReferCount(零 static state),两 caller 均实例方法 → static→instance 等价,commit body 准确。
  • CloneUtil.ts 28→0 注释、SkinnedMeshRenderer/MeshColliderShape/Transition/BaseShape/MeshShape/ParticleCompositeGradient 删"why"注释:这些非任何外部 fix: 的记录依据(joint-texture 行是本 PR 自己 e5894ae42/d09581217 引入,非外部 fix)。关键:这批注释描述的 refcount / cycle-safety / alias-guard invariant 现全部由反向可证伪测试守护 —— 新增 RefCountContract.test.ts(266 行,8 组件 clone acquire/churn/release,含 MeshRenderer.mesh/MeshShape.mesh _cloneTo 路径) + ShaderDataRefCount.test.ts(texture cascade + cloneTo-into-referenced displacement) + CloneManager.test.ts 的 aliasing/cycle/binary-preset 测试。故注释虽删,invariant 未成为静默 delete-hazard(与第 38 轮判据一致)。
  • 文档 clone.mdx en/zh:a4dc14b2 补 function-value 行、Map key cloning、widen throw list、narrow "any depth",逐条对代码核准确;dbb7624c 修 ref-counting 指引(旧文档让 script 作者在 onDestroy 释放 clone 加的 asset 引用,但 public Script surface 做不到 → 改指向 asset 自身 destroy(),真 aspirational-doc 修复);deep-default 双入口文档(DataObject / copyFrom + 免参构造)对 CloneUtil.ts:75/84 准确。

问题

  • [P3] packages/core/src/Signal.ts:12 — 新增单行 // 注释 // Rebuilt by \_cloneTo`; must survive even a propagated @deepClone.末尾带句号。baseSignal.ts无既有单行//惯例可循,按项目单行//` 规则不加句号。纯风格 nit,非阻塞。
  • [P3] docs/*/core/clone.mdx:154-155bacause 拼写(en/zh 各 2 处)。pre-existing,但本 PR 的 prettier commit 8a954232 恰好重排了这两行却没顺手改。非阻塞,建议顺手修。

注释合规

  • 本 delta 新增 7 处单行 //:1 处 Signal.ts(上述 P3)+ 6 处测试文件。测试的 6 处全是多行 //(末行带句号),跟随本文件既有惯例(CloneManager.test.ts 大量同形态多行块),合规。
  • CloneUtil.ts 保留的唯一注释是类级 JSDoc(多行 /** */ 带句号),合规。
  • 无其它新增违规。

已核对为「已解决 / 不适用 / 不重提」

  • [P0] UpdateFlagManager Objectobject —— 第 29 轮修复闭环,本 delta 未触碰,守住。
  • [P3] CloneManager.test.ts stale fixture 注释 —— 第 35 轮闭环。
  • [P2] Entity._createCloneEntity 两遍克隆 block 注释(第 39 轮) —— 本 delta 未触碰 Entity.ts,状态不变,非本 delta scope,不重提。
  • 历史全部闭环项(type-driven gate 核心、dedup 下沉、engine-bound throw、_cloneTo 删除 + 摘 @ignoreClone、DataObject 类级 @internal 删除、order-dependent dedup bug、function 分支归位、unexport CloneUtil、注释陈述化等)—— 本 delta 的 for...inObject.keys@deepClone 传播经对抗验证安全,全部守住。
  • 我第 29 轮 latest state-changing review = APPROVED @ 55f0009af,门控已清;reviewDecision=REVIEW_REQUIRED 是 repo 要求他人 owner 审、非我门控。GuoLei1990 早前 CHANGES_REQUESTED @ aea87ef9d(2026-07-17)即我本人,已被我后续 APPROVED @ 55f0009af(2026-07-18)取代。
  • 自上轮以来无新 PR 评论 / inline / 他人 review 需 reconcile。

判例(第四十一轮)

  • 含真行为变更的大 delta(18 commit)必按"高风险变更逐条对抗验证"审,不能因前 40 轮都是纯注释就放松:本批的 for...inObject.keys(改核心 clone walk)与 @deepClone 传播(改装饰器语义)都是能静默改变克隆结果的变更,各派一个对抗 agent 穷举证伪(前者查全仓 enumerable:true/原型数据 + downlevel 机制;后者查全仓 @deepClone 生产用法 + 所有 SafeLoopArray/DisorderedArray + _cloneTo 交叉),比逐行读推演硬。
  • "删 X 的可枚举遍历"型 perf 变更的验收 = 证明被丢弃集合恰好等于无害集合for...inObject.keys 丢继承可枚举属性,安全当且仅当"所有数据=自有 class field(Object.keys 捕获)+ 所有继承可枚举=方法(该丢)"。全仓 grep enumerable:true 零命中 + downlevel 机制(swc loose emit 原型方法为可枚举)双证。
  • "改装饰器传播语义"的回归面 = grep 该装饰器的全部生产用法@deepClone 一方源码零使用 → forceDeepClone 生产永不为 true → 传播变更零生产回归,是 latent-correctness 而非 active regression。同时穷举唯一能被传播意图触达的 value-type 容器(Signal._listeners)是否已 shield。
  • 注释虽删但 invariant 由反向可证伪测试守护 ≠ 静默 delete-hazard:refcount/cycle/alias invariant 从"why 注释"迁移为 RefCountContract/ShaderDataRefCount 测试断言,revert 任一 invariant 即测试红 —— 这是 invariant 的执行化保全,非静默移除守卫(与第 38 轮判据一致)。
  • GitHub reviews API 默认页有 staleness,校 steady-state 必 --paginate:默认页只返回到 2026-07-17 的 review,漏掉我 rounds 29-40(2026-07-18/19),一度显示我最新 state-changing review 是 CHANGES_REQUESTED 而非真实的 APPROVED @ 55f0009af--paginate 取全量 + compare55f0009af/0494dca5 确为 tip 祖先才还原真相。

简化建议

无 —— 这批 18 commit 是一次高质量的实质推进:@deepClone 恢复 pre-#1682 的子树递归语义并配 honor-or-throw 只作用于装饰值本身、for...inObject.keys 修掉 clone 携带原型方法的 correctness bug(顺带 4x 提速:20-entity 树 0.389ms→0.094ms)、engine-runtime 分支按真实耦合正确拆分、CloneManager 停止 re-export 修跨包 .d.ts 悬空。两处高风险行为变更经对抗验证 + 365 行反向可证伪测试 + CI 12/12 全绿逐条坐实安全,其中两处实为 correctness 修复。仅剩两个 P3 风格/拼写 nit 非阻塞。本 PR 现已可合并

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

增量审查 @ 7f5e8d66(第四十二轮)— delta = 2 commit(纯 TSDoc 收敛)— ⚠️ 执行体 byte-unchanged,1×P2 ownership 边界说明丢失,无 P0/P1

总结

自上轮 c61e0684 起,GitHub compare 为 ahead:2 / behind:0,merge base 正是 c61e0684,无 force-push。两次提交仅修改 DataObject.ts(+2/-3)与 CloneMode.ts(+4/-8)的 TSDoc;剥除注释后,两文件在 checkpoint 与目标 HEAD 的执行体哈希分别完全一致。CI build×3、e2e×4、codecov×3、lint、labeler 共 12/12 全绿。

DataObject 的新文案更准确:CloneUtilinstanceof DataObject 选择默认 field-walk;只有不存在同构 compatible preset 时 _createCloneTarget 才裸构造,因此 “deep-cloned by default” 与 “when no compatible preset exists” 都对齐真实控制流,也保住了此前无参构造契约。

CloneMode 三个 member 的精简本身准确:Ignore 保留 target preset,Assignment 直接返回 source,Deep 递归传播且嵌套 engine-bound 值走默认分支。删掉 Assignment 注释里的 refCount 细节也是正确的 ownership 收敛——字段值策略由 CloneMode/CloneUtil 决定,component 顶层资源槽的 +1/-1ComponentCloner._transferSlotOwnership 结算,公开 clone 文档仍保留完整资源契约。

问题

  • [P2] packages/core/src/clone/enums/CloneMode.ts:2Field clone behavior. 把关键的 owner 边界压没了,而且只是复述 enum 名。CloneMode 并不穷尽字段的 clone 状态:它只承载三个 decorator 的显式 override;未装饰字段以 fieldMode === undefined 进入 CloneUtil._cloneByDefault,Entity/Component remap、ReferResource sharing、runtime preset、container/DataObject deep 等 type-driven default 都由后者权威决定。历史 commit 9ee44e3046 refactor(clone): drop the unused CloneMode.Remap member 的正文也明确记录了 “CloneMode now describes only what a field decorator can request”,当前改动删除了这条边界,容易让后续维护者误以为这里是完整状态机。建议保留 CloneUtil 作为默认策略唯一 owner,并将说明收敛为:

    /**
     * Field-level clone override registered by a clone decorator; undecorated fields use type-driven defaults.
     */

架构治理核对

上游唯一 writer 是 deepClone / assignmentClone / ignoreClone_fieldModes 的注册;下游分别由 CloneUtil 执行值策略、ComponentCloner 结算顶层资源 ownership。此次没有新增重复状态、平行状态机、可推导持久状态或 legacy/compat 路径;唯一需要保留的是上述 override/default 的职责分界,而不是再增加同步层或枚举成员。

已核对为「已解决 / 不适用 / 不重提」

  • DataObject 类级 @internal 导致声明被 stripInternal 抹除的问题保持闭环;本 delta 未改声明或导出链。
  • 历史 clone gate、dedup、honor-or-throw、refCount、ShaderData、bare-construction 等行为修复全部守住;执行体哈希一致。
  • 上轮已列的 Signal 标点、clone.mdx 拼写以及 Entity 两遍克隆注释均未被本 delta 触碰,不作为增量 finding 重复提出。
  • 自上轮以来无新的作者回复、inline comment 或他人 review 需要 reconcile。

简化建议

除恢复 CloneMode 的一句职责边界外,无进一步简化建议。该 P2 是维护者心智与协议归属问题,不阻塞合并;运行时行为没有变化。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

增量审查 @ b10a97c5(第四十三轮)— delta = 1 commit(native shape 挂载 ownership 收口 + kinematic triangle-mesh clone 回归)— ❌ 1×P0:在 native actor 切到 kinematic 前挂载 triangle mesh,新增测试读取乐观镜像而假绿

总结

自上轮 7f5e8d66 起,GitHub compare 为 ahead:1 / behind:0,merge base 正是 7f5e8d66,无 force-push。本轮只含 b10a97c5 fix(physics): defer cloned shape attachment to collider sync,修改 4 个文件(+54/-49)。CI build×3、e2e×4、codecov×3、lint、labeler 共 12/12 全绿,实际 Vitest 为 121 files / 1633 tests 全过。

ColliderShape._collider 设为 @ignoreClone、删除 shape 侧 _attachToCollider / _detachFromCollider,并让 Collider._setNativeShapeAttached 成为 Core 唯一挂载入口,方向正确:owner backlink、native membership 和 _isShapeAttached 不再由 collider/shape 两套状态机竞争。但当前 clone 的 native actor 状态同步顺序违反了 PhysX 的挂载前置条件,而且 adapter/Core 的两层乐观镜像掩盖了失败。

问题

  • [P0] packages/core/src/physics/Collider.ts:168 — cloned kinematic DynamicCollider 会在 native actor 仍为 non-kinematic 时尝试挂载 non-convex triangle mesh;PhysX 拒绝后,代码仍把两层镜像记为已挂载,导致 clone 静默失去真实碰撞形状。

    完整链路是确定的:ComponentCloner 直接写 target[k],所以 _isKinematic=true 只复制到 TS 对象,没有经过 native setter;新加的 _collider @ignoreClone 又使 MeshColliderShape 在 field walk 中先重建为 detached native shape。随后 DynamicCollider._cloneTo() 调用 target._syncNative(),但 DynamicCollider.ts:487 先执行 super._syncNative(),因此这里的 addShape() 先发生;native setIsKinematic(this._isKinematic) 直到 DynamicCollider.ts:504 才执行。

    对 simulation triangle mesh,PxRigidActor::attachShape 的契约要求 PxRigidDynamic 已是 kinematic;checked PhysX 会返回 false,release 下也不能依赖违反前置条件的行为。更严重的是,packages/physics-physx/src/PhysXCollider.ts:32-35 丢弃了 attachShape() 的布尔结果,仍无条件 _shapes.push(shape);本 helper 又无条件写 shape._isShapeAttached = true。之后虽然 actor 被切成 kinematic,却没有任何重试,于是 native actor 没有 shape,而 adapter _shapes 与 Core _isShapeAttached 都声称有。

    新测试 tests/src/core/physics/MeshColliderShape.test.ts:510 只断言 clonedCollider._nativeCollider._shapes.length === 1,读到的正是上述无条件 push 的 JS mirror,因此无法证明 native 挂载成功。请至少在 DynamicCollider._syncNative() 中先把 native actor 的 kinematic 状态写入,再调用 super._syncNative(),并删除后面的重复 setIsKinematic;同时让最终挂载结果成为状态写入依据(例如把 ICollider.addShape / PhysX adapter 的成功结果传回唯一 helper),测试改为断言实际 Px actor 的 shape 数或用 raycast/contact 验证碰撞。否则使用 checked/custom PhysX runtime 的合法 kinematic triangle-mesh clone 会稳定丢碰撞。

架构治理核对

应保留的唯一 Core owner 是 Collider:它拥有 _shapes、shape backlink 以及 attach/detach funnel;不要恢复 shape 侧 helper,也不要把 _collider 改回 clone remap。DynamicCollider 应权威地建立 actor mode 与 non-convex 兼容前置条件;实际 native membership 则只能由 attachShape 的结果确认。_isShapeAttached 仍有必要表达“逻辑 owner 存在但 cooking/native attach 失败”的状态,但必须机械派生自已确认的 native 操作结果。具体删除方向是:删除 DynamicCollider._syncNative() 中偏晚的重复 kinematic 写入,以及 adapter/Core 在未确认 attachShape 成功时的无条件 _shapes.push / _isShapeAttached=true,不要再增加一层同步状态。

已核对为「已解决 / 不适用 / 不重提」

  • 旧的“mesh setter 先 attach、collider sync 再 attach”重复挂载问题,本轮通过 owner 收口已解决;本 finding 是相反方向的 native reject + mirror false-positive,不是旧问题降级重提。
  • 上轮 CloneMode override/default ownership 文案 P2 未被本 delta 触碰,状态不变,不作为本轮增量 finding 重复提出。
  • 历史 clone gate、dedup、honor-or-throw、refCount、ShaderData、bare-construction、DataObject 声明等闭环项均未被本 delta 回退。
  • 自上轮以来无新的作者回复、inline comment 或他人 review 需要 reconcile。

简化建议

修复不需要恢复任何 legacy 路径:把现有 setIsKinematic 前移到 super._syncNative() 之前并删掉原位置,令现有集中式挂载入口只消费 native 的真实结果,再用 native membership/collision 断言替换 JS mirror 断言即可。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

增量审查 @ b0f97d14(第四十四轮)— delta = 4 commit(field metadata Symbol 化 + clone hook 显式协议 + metadata 收口)— ❌ 1×P0 未闭环 + 1×P1 新增

总结

自上轮 b10a97c5 起,GitHub compare 为 ahead:4 / behind:0,merge base 正是 b10a97c5,无 force-push。本轮 4 个 commit 修改 70 个文件;CI build×3、e2e×4、codecov×3、lint、labeler 共 12/12 全绿。

把字段 metadata 从可碰撞的字符串 _fieldModes 改为私有 Symbol、让 CloneDecorators 成为唯一注册 owner,并删除 CloneManager / CloneMode 过渡层,方向正确;ICloneHook 也正确把 post-clone 协议显式化,并把 identity map 限为只读。但上一轮 native 挂载 P0 的执行顺序和假绿断言逐行未变,同时 hook 改名静默切断了既有用户扩展点。

问题

  • [P0] packages/core/src/physics/DynamicCollider.ts:487 — kinematic triangle-mesh clone 仍在 native actor 切到 kinematic 前挂载 shape;PhysX 拒绝后,adapter 与 Core 仍乐观记录成功,clone 实际没有碰撞形状。

    ComponentCloner 只把 _isKinematic=true 写进 TS target;DynamicCollider._onClone() 随后调用 super._onClone(),经 Collider._syncNative():487 先挂载 shape,而 native setIsKinematic(this._isKinematic):504 才执行。PhysX 的 attachShape 对 non-kinematic dynamic actor + simulation triangle mesh 返回失败,但 PhysXCollider.addShape() 丢弃返回值并无条件 _shapes.pushCollider._setNativeShapeAttached() 又无条件写 _isShapeAttached=true。测试 MeshColliderShape.test.ts:510 仍只读取这份乐观 JS mirror,因此继续假绿。

    请先把 native actor 的 kinematic mode 写入,再执行 super._syncNative();同时让 native attachShape 结果成为唯一事实 owner:ICollider.addShape 返回成功结果,PhysX adapter 只在成功时更新 _shapes / scene shape registration,Core 只据此更新 _isShapeAttached。测试应断言真实 Px actor shape 数或 raycast/contact,而不是 adapter mirror。

  • [P1] packages/core/src/clone/ICloneHook.ts:11 — 新协议把文档化的 _cloneTo 扩展点改名为 _onClone,现有用户组件会无编译错误地静默丢失 clone hook。

    dev/2.0 基线文档明确要求“implement the _cloneTo() method”,基线 ComponentCloner 也实际 duck-call _cloneTo;这不是本 PR 中间态才出现的名字。Git 历史中 f1aab7d2 chore: add engine version documentation 已把它写入用户文档,886dda635 Fix compont props clone bug (#2926) 又保留并集中该 dispatch。目标 HEAD 的 CloneUtil / ComponentCloner 只调用 _onClone,所以已有 Script 子类即使仍声明 _cloneTo(target) 也不会报类型错误,只会停止重建 native state、监听或资源 ownership;PR 正文目前还残留多处 _cloneTo,也未记录这项 breaking migration。

    应保留本轮新建的 ICloneHook 作为唯一协议 owner,但把其 member 命名为既有的 _cloneTo,并机械回改实现、调用、测试和文档链接;不要增加 _cloneTo / _onClone 双 dispatch 或兼容状态机。这样既获得显式类型与 ReadonlyMap 边界,也不破坏既有扩展点。

架构治理核对

字段模式链路已正确收口为 CloneDecorators 注册 Symbol metadata → CloneUtil / ComponentCloner 消费,旧 CloneManager、公开 CloneMode 和字符串 metadata 均已删除,没有残留平行 registry。post-clone 行为应继续只由 ICloneHook 定义,但应复用既有方法名,删除本轮多出来的协议改名而不是维护两条路径。

物理链路的权威 owner 必须是 Px actor 的实际 attach 结果;PhysXCollider._shapes 是 scene registration 所需的 confirmed projection,ColliderShape._isShapeAttached 是 Core confirmed cache,两者都不能在 native reject 后自行写成 true。

已核对为「已解决 / 不适用 / 不重提」

  • 上轮以前的重复 attach helper / shape backlink 双 ownership 已由 Collider 收口,保持解决;本轮 P0 是其下游 native reject 未反馈,不是恢复 shape 侧 helper。
  • 上轮 CloneMode override/default ownership 文案 P2 随 CloneMode 整体删除而不再适用。
  • metadata 字符串碰撞已由 c28608ed 的 Symbol key 与反向测试闭环。
  • 历史 clone gate、dedup、honor-or-throw、refCount、ShaderData、bare-construction、DataObject 声明等闭环项均未被本 delta 回退。
  • 自上轮以来无新的作者回复、inline comment 或他人 review 需要 reconcile。

简化建议

不需要新增同步层:clone 侧用“现有 _cloneTo 名称 + 新 ICloneHook 类型”单协议即可;physics 侧把 kinematic 写入前移,并让现有两层镜像只消费 native attach 的真实结果。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

增量审查 @ ebad7d9a(第四十五轮)— delta = 2 commit(native attach 真值闭环 + structural clone 边界加固)— ❌ 2×P1:1 个新数据丢失边界 + 1 个既有扩展协议破坏仍未闭环

总结

自上轮 b0f97d14 起,GitHub compare 为 ahead:2 / behind:0,merge base 正是 b0f97d14,无 force-push;本轮修改 9 个文件(+262/-57)。build×3、e2e×4、codecov×3、lint、labeler 共 12/12 全绿,期间没有新的作者回复、inline comment 或他人 review。

1aa63e94 已正确闭环上一轮物理 P0:native actor 先切 kinematic 再挂 shape,PxRigidActor::attachShape 的失败在 adapter push / scene registration 前抛出,Core 也只在调用成功后写 _isShapeAttached、backlink 和 _shapes;回归测试改为销毁原对象后 raycast 命中 clone,证明了真实 native membership,而不是读取 JS mirror。

ebad7d9a 也修复了 own constructor 字段污染 plain-object 判定及 cross-realm plain object 识别,并让 Date/RegExp 作为直接 @deepClone 值时 honor-or-throw。但新增的“field-cloneable”判据仍不能证明 enumerable field walk 能重建全部状态;同时上一轮指出的 hook 静默改名仍未处理。

问题

  • [P1] packages/core/src/clone/CloneUtil.ts:323_canCloneByFields()[object Object] 当成“全部状态可由 enumerable fields 重建”的能力证明,会让 @deepClone 静默丢失普通用户类的私有状态。

    这是确定可触发的数据丢失:一个含 #value 的用户类默认 tag 就是 [object Object]。源对象把 #value 改为 42 后,当前链路会通过 _canCloneByFields,在 _createCloneTargetnew ctor() 得到默认值 0,再因 Object.keys(source) 为空而复制不到任何状态;最终 cloned !== source:53 的 honor-or-throw 反查也不会抛错,clone 对外读到 0。反方向同样不可靠:一个只有 enumerable state、但自定义 Symbol.toStringTag 的类会被误判不可 field-clone;新增 TaggedCopyValue 测试之所以通过,只是因为 copyFrom 分支在该判据之前早退。

    Object.prototype.toString 只描述 tag,不是 clone capability。请保留 plain/null-prototype record、DataObjectcopyFrom 作为权威 owner,删除 _canCloneByFields 这条推断路径;若仍要让 @deepClone 的软意图进入任意用户 class,应新增一个显式、可验证的 field-clone capability/协议,让该 owner 决定是否 walk,并让无能力的直接装饰值在构造/写字段前抛错。不要再加平台类型白名单或第二份 tag/constructor 启发式。

  • [P1] packages/core/src/clone/ICloneHook.ts:11 — 上轮指出的 _cloneTo_onClone 静默 breaking change 仍未闭环;本轮 CloneUtilComponentCloner 仍只 dispatch _onClone

    dev/2.0 的真实 ComponentCloner:40 duck-call _cloneTo,基线用户文档也明确要求实现 _cloneTo();已有 Script 子类不会因这次改名产生编译错误,只会停止重建 native state、监听或资源 ownership。PR 正文至今也仍写着“_cloneTo hooks receive (target, cloneMap)”,与目标代码不一致。

    应保留新 ICloneHook 作为 post-clone 协议唯一 owner,但把 member 恢复为既有 _cloneTo,并机械回改全部实现、dispatch、测试、文档与 PR 描述;删除本轮引入的 _onClone 名称,不要维护双 dispatch 或兼容状态机。

架构治理核对

物理链路现已收口为 Px actor 的 attachShape 结果是唯一事实;PhysXCollider._shapes 只是成功 membership 的 scene-registration projection,Core _isShapeAttached / backlink / _shapes 都在成功后机械更新,没有恢复旧的 shape 侧平行 attach 状态机。

clone 链路上,field decorator metadata 仍由 CloneMetadata 唯一写入、CloneUtil / ComponentCloner 消费;当前新增的重复 ownership 是 _canCloneByFields 用 tag 猜测 capability,而 DataObject / copyFrom / plain record 已经各自承担可验证的结构复制协议。应删除 tag 猜测并让显式协议拥有 custom-class cloneability。post-clone 行为则只保留一个 ICloneHook._cloneTo 协议 owner。

已核对为「已解决 / 不适用 / 不重提」

  • 上轮 native attach P0 已由 1aa63e94 的执行顺序、失败传播和真实 raycast 断言闭环;adapter/Core 不再在 native reject 后写乐观镜像。
  • 旧 own-constructor shadow 与 cross-realm plain-object 判定问题已由 prototype-based _isPlainObject / _createCloneTarget 和反向测试闭环;本轮 P1 是新 _canCloneByFields 无法证明 opaque user state,不是换角度重提 constructor 问题。
  • metadata Symbol 化、单一 registry、clone gate、dedup、honor-or-throw、refCount、ShaderData、bare construction、DataObject 声明等历史闭环项未被本 delta 回退。

简化建议

物理侧无需再加同步层。clone 侧最小形态是“plain record + DataObject / copyFrom / 显式 custom capability”决定结构复制,ICloneHook._cloneTo 决定 post-clone 行为;删除 tag 启发式和 _onClone 改名即可,不要维护猜测、兼容与双协议。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

校正更新 @ e882bae2❌ 1×P1 未闭环 + 1×P2 TSDoc 边界不精确

更正

撤回原 review 中“_cloneTo_onClone 是 P1 breaking change”的判断。

当前 clone gate 已经拥有自动字段复制的唯一责任,hook 只在字段完成后追加类型行为,因此 _onClone_cloneTo 更准确。新 ICloneHook 也把该协议显式化,并将 identity map 限为只读;目标代码、中英文文档和全部实现已经统一为 _onClone。目标分支是 dev/2.0,旧 ICustomClone._cloneTo 又是 @internal 协议,不应为了旧名称增加双 dispatch 或兼容层。

PR Summary 仍有 5 处 _cloneTo 历史描述,应机械更新为 _onClone;这是非阻塞的描述清理,不再作为 P1。

总结

自本 review 原锚点 00105734 起,后续提交把默认策略收敛为内部 Symbol metadata:Entity / Component → RemapReferResource → Assignment、runtime 容器 → Ignore、DataObject → Deep、数学值类型 → Copy。这个方向正确:用户字段零配置进入 Default,类型例外由内部 owner 声明,不公开 @defaultCloneMode

但新的正向 mode registry 目前只负责 dispatch,没有成为显式 Deep 的 cloneability 边界;CloneUtil 又新增了 Date / RegExp 负向黑名单,因此 opaque state 的静默数据丢失仍未闭环。

问题

  • [P1] packages/core/src/clone/CloneUtil.ts:111_isKnownOpaqueObject() 只识别 DateRegExp,不能证明其他非普通对象可以由 enumerable field walk 重建。

    两个确定反例仍会静默损坏:裸 ArrayBuffer 不是 view,会进入 _cloneObjectByFields()new ArrayBuffer() 只得到 0 字节目标且 Object.keys(source) 为空;含 #value 的用户类会通过构造得到默认私有值,但 field walk 无法读取或复制该状态。两种结果都满足 cloned !== source,所以 honor-or-throw 不会报错。

    这轮新增的 defaultCloneModeKey 已经提供了正向能力 owner。建议让 plain/null-prototype record、Array/Map/Set/View,以及注册为 Copy / Deep 的类型进入对应克隆路径;直接 @deepClone 遇到未注册的非普通 class 时在构造和写字段前抛错,并删除 _isKnownOpaqueObject() 平台黑名单。不要继续补充 Date/RegExp/URL/Promise 等类型白名单。

  • [P2] packages/core/src/base/DataObject.ts:4 — “objects whose fields are structurally cloned” 仍超过实际契约。执行器只遍历 Object.keys(source),即 own enumerable string-keyed properties;private field、non-enumerable property、Symbol key 和 prototype accessor 均不会自动复制。

    建议改为:Base class for objects whose own enumerable string-keyed properties are structurally cloned by default.

架构治理核对

CloneDecorators 的 Symbol metadata 现在是默认策略唯一注册 owner,CloneUtil / ComponentCloner 只消费它;该 registry 也应成为结构复制能力的正向边界,避免同时维护一份 opaque 类型黑名单。

post-clone 行为则已经正确收口为 ICloneHook._onClone 单协议:保留新名称,不增加 _cloneTo 兼容路径,只需清理 PR Summary 的旧术语。

当前结论

  • P0:0。此前 native shape attach 的执行顺序、失败传播与真实 raycast 验证保持闭环,后续 delta 未触碰物理链路。
  • P1:1。仅剩显式 Deep 对 opaque/private state 的静默数据损坏。
  • P2:1。DataObject TSDoc 未准确陈述 Object.keys 可见边界。
  • _onClone:设计合理,原 P1 撤回。

因此 review 继续保持 Changes requested,阻塞项只有上述 cloneability capability boundary。

@GuoLei1990
GuoLei1990 merged commit 05ba311 into galacean:dev/2.0 Jul 24, 2026
12 checks passed

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫧 尘小沫

增量审查 @ 24017e79(第四十六轮)— delta = 9 commit(3 个 clone follow-up + 1 个 merge + 5 个已进入 dev/2.0 的上游提交)— ❌ 1×P1:普通对象 tag 仍是第二份 cloneability 真相,私有状态继续静默丢失

总结

自上一审查锚点 e882bae2 起,GitHub compare 为 ahead:9 / behind:0,merge base 正是 e882bae2,无 force-push。其中 5 个提交来自最新 dev/2.0;merge commit 没有改 clone 文件。本轮 clone 自身变化是:ccfd52e5 用 branded-object 边界替换 Date/RegExp 黑名单并修正 DataObject TSDoc,766b4762 明确 DataObject/内部 math Copy 两类显式能力并同步文档与测试,24017e79 把只有类内消费者的 helper 改为 private。build×3、e2e×4、codecov×3、lint、labeler 共 12/12 全绿,自上一轮以来没有新的作者回复、inline comment 或他人 review。

DataObject 的 enumerable string-keyed 契约已准确,math copyFrom 也不再靠方法名推断;branded platform objects 作为直接 @deepClone 值会抛错、作为深子树成员会保持 Assignment,均比负向平台类型黑名单更完整。但未注册普通 class 仍由 tag 启发式判为可字段克隆,所以之前的 opaque/private-state P1 只覆盖了 branded 对象,没有闭环。

问题

  • [P1] packages/core/src/clone/CloneUtil.ts:131_hasOrdinaryObjectTag() 仍把 [object Object] 当成结构克隆能力,未注册普通类的私有或闭包状态会继续静默损坏。

    一个确定反例是 class PrivateValue { #value = 0; set value(v) { this.#value = v; } get value() { return this.#value; } }。源值设为 42 后,Object.prototype.toString.call(source)[object Object]Object.keys(source) 是空数组;当前直接 @deepClone 路径会通过 :131,在 _createCloneTarget()new PrivateValue() 得到 0,再因没有 enumerable key 而复制不到 #value。最终 clone 返回一个新实例且值为 0,既不共享也不抛错。深子树路径在 :177 使用同一 tag 推断,因此同样会损坏。

    这不是文档精度问题:本轮已建立 defaultCloneModeKey 正向 registry,DataObject → Deep 与内部 math 类型 → Copy 才是可验证的 class capability owner;_hasOrdinaryObjectTag 又维护了一条无法证明对象完整状态可由 field walk 重建的平行能力路径。TaggedCopyValue 测试只因人为添加 Symbol.toStringTag 才被拒绝,不能覆盖同样未注册但无 brand 的普通用户类。

    请保留 plain/null-prototype record、Array/Map/Set/View,以及注册为 Deep / Copy 的类型作为结构复制入口,删除 _hasOrdinaryObjectTag() 对未注册 class 的能力推断:直接被 @deepClone 装饰的未注册 class 应在构造和写字段前抛错,深子树里的未注册 class 保持其 Assignment 默认。tests/src/core/Clone.test.ts:780:1553:1573 目前明确锁定了“未注册 class 也 field-walk”的旧路径;若需要结构语义,应把 fixture 改成 DataObject 或 plain record,并补一个 private-field 反向测试。不要为这些旧断言保留 compatibility branch、wrapper 或第二条转换路径。

架构治理与熵增核对

上游 writer 是 CloneDecorators 的 Symbol metadata 与 DataObject/math 注册;CloneUtil 应只消费这一能力事实,ComponentCloner 再把结果写入 slot 并结算顶层资源 ownership。相较上一轮,平台黑名单虽已删除,但系统仍是“显式 registry + object tag 推断”两个 class capability owner,owner 数没有真正收敛;新增文档和测试反而把第二条路径固化。删除 tag 推断后,数据流可收口为“plain/container 内建规则,registered class capability,其他 class 默认共享或在直接显式 Deep 时抛错”,无需新增同步状态或 legacy fallback。

24017e79 的 private 化本身是净减法:全仓仅 _cloneObjectFields_cloneFieldValue_transferSlotOwnership 有类外消费者,其余 helper 都只有 CloneUtil 内部调用,没有遗留 wrapper。merge 也没有把上游复杂度转移进 clone 链路。

已核对为已解决 / 不适用 / 不重提

  • DataObject TSDoc 已由 ccfd52e5 精确收窄为 own enumerable string-keyed properties,上一轮 P2 闭环。
  • Date/RegExp/ArrayBuffer/Error/WeakMap 等 branded opaque object 的直接 throw 与嵌套 Assignment 已有正反测试;本 P1 只针对仍被 [object Object] 放行的未注册普通 class。
  • native shape attach 的执行顺序、失败传播与真实 raycast 验证保持闭环,本 delta 未触碰物理链路。
  • _onClone 是当前单一 post-clone 协议 owner;不恢复 _cloneTo,也不增加双 dispatch。PR Summary 已统一新名称。
  • math Copy 注册、helper private 化和最新 dev/2.0 merge 未引入重复 ownership、兼容分支或失效测试。

当前结论

P0:0;P1:1;P2:0。继续 Changes requested,唯一阻塞项是让正向 registry 成为 class cloneability 的唯一 owner,并删除普通 object tag 的推断路径及锁定该旧行为的测试。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants